import datetime, logging, os, sys

log = None

APPLICATION_INSTALL_DIR = os.environ.get('SIMPLEAUTH_INSTALL')
if not APPLICATION_INSTALL_DIR:
    raise EnvironmentError("Must Set SIMPLEAUTH_INSTALL in the environment!")

sys.path.append(os.path.join(APPLICATION_INSTALL_DIR, "lib%spython" % os.sep))
sys.path.append(os.path.join(APPLICATION_INSTALL_DIR, "plugins"))

from twisted.web import xmlrpc
from twisted.internet import reactor

from simple_auth.config import DEFAULT_CONFIG_PATH, ConfigLoader
from simple_auth.config.classbuilder import KlassBuilder
from simple_auth import auth, envbuilder
from simple_auth.auth import resource as sa_resource

class SimpleAuthError(Exception):
    """simple auth error"""


class SimpleAuthService(xmlrpc.XMLRPC):
    """simple authentication service"""
    
    def __init__(self, configName, configPath=None, selfSufficient=True):
        
        self.reactor = None
        if selfSufficient:
            self.reactor = reactor
        if configPath is None:
            configPath = DEFAULT_CONFIG_PATH
        server_name = "service_" + configName
        try:
            self.config = ConfigLoader(configPath).load(server_name)
        except:
            try:
                possibleConfig = os.path.join(os.path.dirname(__file__), "conf")
                self.config = ConfigLoader(possibleConfig).load(server_name)
            except:
                raise
        
        # initialize logger
        today = datetime.date.today().strftime("%Y-%m-%d")

        logging.basicConfig(level=logging.__getattribute__(str(self.config.logging_level)),
                            format='%(asctime)s %(name)-8s %(levelname)-8s %(message)s',
                            filename='%s%sSimpleAuthService.%s.log' % (str(self.config.logging_path), os.sep, today),
                            filemode='a')

        console = logging.StreamHandler()
        console.setLevel(logging.INFO)
        formatter = logging.Formatter('%(name)s %(levelname)-8s %(message)s')
        console.setFormatter(formatter)
        logging.getLogger('').addHandler(console)
        log = logging.getLogger('')
        
        # finish initializing service
        logging.info("SimpleAuthService starting")
        self.configPath = configPath
        self.pluginConf = os.path.join(APPLICATION_INSTALL_DIR, "plugins%sconf" % os.sep)
        self.authOverrides = self.loadAuthOverrides()
        self.cryptPlugins = self.loadCryptPlugins()
        logging.info( "SimpleAuthService now running..." )
    
    
    def loadAuthOverrides(self):
        """looks into the configuration directories, and builds authentication
           override classes.  override classes must conform to the pattern:
               plugin.authenticate(username, password)
           in order to be valid plugins
        """
        plugins = {}
        for filename in os.listdir(self.pluginConf):
            pluginName, plugin = self._tryPlugin(filename, pluginType="auth")
            if plugin and pluginName:
                plugins[pluginName] = plugin
        logging.info( "loaded auth overrides: %s" % plugins.keys() )
        return plugins

    
    def _tryPlugin(self, file, pluginType=None):
        if pluginType is None:
            raise SimpleAuthError("Must set plugin type to load a plugin!")
        if pluginType.lower() == "auth":
            plugin_base = "plugin_"
        elif pluginType.lower() == "crypt":
            plugin_base = "encrypt_"
        plugin = None
        pluginName = None
        try:
            if os.path.basename(file)[0] == ".":
                return (None, None)
        except IndexError:
            return (None, None)

        try:
            for f in os.listdir(file):
                self._tryPlugin(os.path.abspath(os.path.join(file, f)), pluginType)
        except OSError:
            file_path = os.path.dirname(file)
            file = os.path.basename(file)
            if (file.startswith(plugin_base)):
                name = file[file.index("_")+1:-4]
                pname = "%s%s" % (plugin_base, name)
                pluginName = name
                if str(ConfigLoader(self.pluginConf).load(pname).enabled).lower() == "true":
                    plugklass = KlassBuilder(pname, configPath=self.pluginConf)()
                    plugin = plugklass(name, configPath=self.pluginConf)
        return (pluginName, plugin)


    def loadCryptPlugins(self):
        """looks into the configuration directories, and builds encryption
           plugins to handle the necessary encrypt/decrypt actions"""
        crypt_plugins = {}
        for filename in os.listdir(self.pluginConf):
            try:
                pluginName, plugin = self._tryPlugin(filename, pluginType="crypt")
                if plugin and pluginName:
                    crypt_plugins[pluginName] = plugin
            # blank excepts are bad, but we don't want to die if a plugin fails to load
            except Exception, e:
                print "WARNING: plugin failed to load with exception: %s" % str(e)
                pass
        logging.info( "loaded crypt plugins: %s" % crypt_plugins.keys() )
        return crypt_plugins
    
    
    def getCrypt(self, cryptType):
        """should dig into the available encryption wrappers
           and build and return the necessary one.  all encryption
           wrappers should expose the api of:
               encryption.encrypt()
               encryption.decrypt()
        """
        encryption = None
        if cryptType in self.cryptPlugins:
            encryption = self.cryptPlugins.get(cryptType)
        return encryption

    
    def handleCrypt(self, crypt, password):
        """
           crypt should be a tuple of (action, cryptType)
           available actions should be "encrypt" or "decrypt" based on the
           auth plugin being used
        """
        logging.debug("entered handleCrypt(%s, %s)" % (crypt, password))
        cryptAction, cryptType = crypt
        encryption = self.getCrypt(cryptType)
        if encryption:
            if hasattr(encryption, cryptAction):
                func = getattr(encryption, cryptAction)
                password = func(password)    
        return password


    def xmlrpc_authenticate(self, username, password, crypt=(None, None), authOverride=None, resource_name=None):
        """returns bool if a user is authenticated
           crypt = (action, cryptType)
           authOverride is a flag to indicate a particular authentication
           plugin, rather than the simple auth db.
           NOTE: 
             because this is an authentication service, we don't want to raise when
             bad/wrong/broken requests come through.  if a plugin is misnamed, 
             doesn't expose the right api, or whatever, we don't want to raise
             and lend that to exploitative mapping of what plugins are available.
             therefore, we merely set the response to False, and return
        """
        response = None
        password = self.handleCrypt(crypt, password)
        if authOverride:
            plugin = self.authOverrides.get(authOverride)
            if plugin:
                try:
                    response = plugin.authenticate(username, password)
                except AttributeError:
                    respone = False
            else:
                response = False
        elif resource_name:
            response = sa_resource.authenticate(username, password, resource_name)
        else:
            response = auth.authenticate(username, password)
        if response is None:
            response = False
        
        logging.debug("authenticate(username=[%(username)s], password=[%(password)s], crypt=[%(crypt)s], authOverride=[%(authOverride)s], resource_name=[%(resource_name)s]) returning %(response)s" % vars())
        return response


    def xmlrpc_authorize(self, servicename, username, rolename, authOverride=None, resource_name=None):
        """far more simple than authorize, really
           still, authOverride can be handed in to build an authorization
           plugin, if need be.  all plugins must support the
               plugin.authorize(servicename, username, rolename)
           style api to be valid
        """
        response = None
        if authOverride:
            plugin = self.authOverrides.get(authOverride)
            if plugin:
                try:
                    response = plugin.authorize(servicename, username, rolename)
                except AttributeError:
                    response = False
            else:
                response = False
        elif resource_name:
            response = sa_resource.authorize(servicename, username, rolename, resource_name)
        else:
            response = auth.authorize(servicename, username, rolename)
        logging.debug("authorize(servicename=[%(servicename)s], username=[%(username)s], rolename=[%(rolename)s], authOverride=[%(authOverride)s], resource_name=[%(resource_name)s]) returning %(response)s" % vars())
        return response


if __name__ == "__main__":
    from optparse import OptionParser
    usage = "usage: %prog {-n, --name} servername {-p, --path} configpath"
    parser = OptionParser(usage)
    parser.add_option("-n", "--name", dest="name", help="name of the server to start")
    parser.add_option("-p", "--path", dest="path", help="configuration path")
    (options, args) = parser.parse_args()
    name = options.name
    configPath = options.path

    port = None
    if not name:
        env = envbuilder.EnvironmentBuilder()
        name = env.default_server
        port = int(env.default_port)

    from twisted.web import server, resource
    """run server"""
    root = resource.Resource()
    rs = SimpleAuthService(name, configPath)
    root.putChild('RPC2', rs)
    site = server.Site(root)
    if not port:
        port = int(rs.config.listen_port.xml_text_content())
    rs.reactor.listenTCP(port, site)
    rs.reactor.run()
